/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const Cu = Components.utils;

const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const { console } = require("resource://gre/modules/Console.jsm");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");

XPCOMUtils.defineLazyModuleGetter(this,
  "Reflect", "resource://gre/modules/reflect.jsm");

this.EXPORTED_SYMBOLS = ["Parser", "ParserHelpers", "SyntaxTreeVisitor"];

/**
 * A JS parser using the reflection API.
 */
this.Parser = function Parser() {
  this._cache = new Map();
  this.errors = [];
  this.logExceptions = true;
};

Parser.prototype = {
  /**
   * Gets a collection of parser methods for a specified source.
   *
   * @param string source
   *        The source text content.
   * @param string url [optional]
   *        The source url. The AST nodes will be cached, so you can use this
   *        identifier to avoid parsing the whole source again.
   */
  get(source, url = "") {
    // Try to use the cached AST nodes, to avoid useless parsing operations.
    if (this._cache.has(url)) {
      return this._cache.get(url);
    }

    // The source may not necessarily be JS, in which case we need to extract
    // all the scripts. Fastest/easiest way is with a regular expression.
    // Don't worry, the rules of using a <script> tag are really strict,
    // this will work.
    let regexp = /<script[^>]*?(?:>([^]*?)<\/script\s*>|\/>)/gim;
    let syntaxTrees = [];
    let scriptMatches = [];
    let scriptMatch;

    if (source.match(/^\s*</)) {
      // First non whitespace character is &lt, so most definitely HTML.
      while ((scriptMatch = regexp.exec(source))) {
        // Contents are captured at index 1 or nothing: Self-closing scripts
        // won't capture code content
        scriptMatches.push(scriptMatch[1] || "");
      }
    }

    // If there are no script matches, send the whole source directly to the
    // reflection API to generate the AST nodes.
    if (!scriptMatches.length) {
      // Reflect.parse throws when encounters a syntax error.
      try {
        let nodes = Reflect.parse(source);
        let length = source.length;
        syntaxTrees.push(new SyntaxTree(nodes, url, length));
      } catch (e) {
        this.errors.push(e);
        if (this.logExceptions) {
          DevToolsUtils.reportException(url, e);
        }
      }
    } else {
      // Generate the AST nodes for each script.
      for (let script of scriptMatches) {
        // Reflect.parse throws when encounters a syntax error.
        try {
          let nodes = Reflect.parse(script);
          let offset = source.indexOf(script);
          let length = script.length;
          syntaxTrees.push(new SyntaxTree(nodes, url, length, offset));
        } catch (e) {
          this.errors.push(e);
          if (this.logExceptions) {
            DevToolsUtils.reportException(url, e);
          }
        }
      }
    }

    let pool = new SyntaxTreesPool(syntaxTrees, url);

    // Cache the syntax trees pool by the specified url. This is entirely
    // optional, but it's strongly encouraged to cache ASTs because
    // generating them can be costly with big/complex sources.
    if (url) {
      this._cache.set(url, pool);
    }

    return pool;
  },

  /**
   * Clears all the parsed sources from cache.
   */
  clearCache() {
    this._cache.clear();
  },

  /**
   * Clears the AST for a particular source.
   *
   * @param String url
   *        The URL of the source that is being cleared.
   */
  clearSource(url) {
    this._cache.delete(url);
  },

  _cache: null,
  errors: null
};

/**
 * A pool handling a collection of AST nodes generated by the reflection API.
 *
 * @param object syntaxTrees
 *        A collection of AST nodes generated for a source.
 * @param string url [optional]
 *        The source url.
 */
function SyntaxTreesPool(syntaxTrees, url = "<unknown>") {
  this._trees = syntaxTrees;
  this._url = url;
  this._cache = new Map();
}

SyntaxTreesPool.prototype = {
  /**
   * @see SyntaxTree.prototype.getIdentifierAt
   */
  getIdentifierAt({ line, column, scriptIndex, ignoreLiterals }) {
    return this._call("getIdentifierAt",
      scriptIndex, line, column, ignoreLiterals)[0];
  },

  /**
   * @see SyntaxTree.prototype.getNamedFunctionDefinitions
   */
  getNamedFunctionDefinitions(substring) {
    return this._call("getNamedFunctionDefinitions", -1, substring);
  },

  /**
   * @return SyntaxTree
   *         The last tree in this._trees
   */
  getLastSyntaxTree() {
    return this._trees[this._trees.length - 1];
  },

  /**
   * Gets the total number of scripts in the parent source.
   * @return number
   */
  get scriptCount() {
    return this._trees.length;
  },

  /**
   * Finds the start and length of the script containing the specified offset
   * relative to its parent source.
   *
   * @param number atOffset
   *        The offset relative to the parent source.
   * @return object
   *         The offset and length relative to the enclosing script.
   */
  getScriptInfo(atOffset) {
    let info = { start: -1, length: -1, index: -1 };

    for (let { offset, length } of this._trees) {
      info.index++;
      if (offset <= atOffset && offset + length >= atOffset) {
        info.start = offset;
        info.length = length;
        return info;
      }
    }

    info.index = -1;
    return info;
  },

  /**
   * Handles a request for a specific or all known syntax trees.
   *
   * @param string functionName
   *        The function name to call on the SyntaxTree instances.
   * @param number syntaxTreeIndex
   *        The syntax tree for which to handle the request. If the tree at
   *        the specified index isn't found, the accumulated results for all
   *        syntax trees are returned.
   * @param any params
   *        Any kind params to pass to the request function.
   * @return array
   *         The results given by all known syntax trees.
   */
  _call(functionName, syntaxTreeIndex, ...params) {
    let results = [];
    let requestId = [functionName, syntaxTreeIndex, params].toSource();

    if (this._cache.has(requestId)) {
      return this._cache.get(requestId);
    }

    let requestedTree = this._trees[syntaxTreeIndex];
    let targettedTrees = requestedTree ? [requestedTree] : this._trees;

    for (let syntaxTree of targettedTrees) {
      try {
        let parseResults = syntaxTree[functionName].apply(syntaxTree, params);
        if (parseResults) {
          parseResults.sourceUrl = syntaxTree.url;
          parseResults.scriptLength = syntaxTree.length;
          parseResults.scriptOffset = syntaxTree.offset;
          results.push(parseResults);
        }
      } catch (e) {
        // Can't guarantee that the tree traversal logic is forever perfect :)
        // Language features may be added, in which case the recursive methods
        // need to be updated. If an exception is thrown here, file a bug.
        DevToolsUtils.reportException(
          `Syntax tree visitor for ${this._url}`, e);
      }
    }
    this._cache.set(requestId, results);
    return results;
  },

  _trees: null,
  _cache: null
};

/**
 * A collection of AST nodes generated by the reflection API.
 *
 * @param object nodes
 *        The AST nodes.
 * @param string url
 *        The source url.
 * @param number length
 *        The total number of chars of the parsed script in the parent source.
 * @param number offset [optional]
 *        The char offset of the parsed script in the parent source.
 */
function SyntaxTree(nodes, url, length, offset = 0) {
  this.AST = nodes;
  this.url = url;
  this.length = length;
  this.offset = offset;
}

SyntaxTree.prototype = {
  /**
   * Gets the identifier at the specified location.
   *
   * @param number line
   *        The line in the source.
   * @param number column
   *        The column in the source.
   * @param boolean ignoreLiterals
   *        Specifies if alone literals should be ignored.
   * @return object
   *         An object containing identifier information as { name, location,
   *         evalString } properties, or null if nothing is found.
   */
  getIdentifierAt(line, column, ignoreLiterals) {
    let info = null;

    SyntaxTreeVisitor.walk(this.AST, {
      /**
       * Callback invoked for each identifier node.
       * @param Node node
       */
      onIdentifier(node) {
        if (ParserHelpers.nodeContainsPoint(node, line, column)) {
          info = {
            name: node.name,
            location: ParserHelpers.getNodeLocation(node),
            evalString: ParserHelpers.getIdentifierEvalString(node)
          };

          // Abruptly halt walking the syntax tree.
          SyntaxTreeVisitor.break = true;
        }
      },

      /**
       * Callback invoked for each literal node.
       * @param Node node
       */
      onLiteral(node) {
        if (!ignoreLiterals) {
          this.onIdentifier(node);
        }
      },

      /**
       * Callback invoked for each 'this' node.
       * @param Node node
       */
      onThisExpression(node) {
        this.onIdentifier(node);
      }
    });

    return info;
  },

  /**
   * Searches for all function definitions (declarations and expressions)
   * whose names (or inferred names) contain a string.
   *
   * @param string substring
   *        The string to be contained in the function name (or inferred name).
   *        Can be an empty string to match all functions.
   * @return array
   *         All the matching function declarations and expressions, as
   *         { functionName, functionLocation ... } object hashes.
   */
  getNamedFunctionDefinitions(substring) {
    let lowerCaseToken = substring.toLowerCase();
    let store = [];

    function includesToken(name) {
      return name && name.toLowerCase().includes(lowerCaseToken);
    }

    SyntaxTreeVisitor.walk(this.AST, {
      /**
       * Callback invoked for each function declaration node.
       * @param Node node
       */
      onFunctionDeclaration(node) {
        let functionName = node.id.name;
        if (includesToken(functionName)) {
          store.push({
            functionName: functionName,
            functionLocation: ParserHelpers.getNodeLocation(node)
          });
        }
      },

      /**
       * Callback invoked for each function expression node.
       * @param Node node
       */
      onFunctionExpression(node) {
        // Function expressions don't necessarily have a name.
        let functionName = node.id ? node.id.name : "";
        let functionLocation = ParserHelpers.getNodeLocation(node);

        // Infer the function's name from an enclosing syntax tree node.
        let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(node);
        let inferredName = inferredInfo.name;
        let inferredChain = inferredInfo.chain;
        let inferredLocation = inferredInfo.loc;

        // Current node may be part of a larger assignment expression stack.
        if (node._parent.type == "AssignmentExpression") {
          this.onFunctionExpression(node._parent);
        }

        if (includesToken(functionName) || includesToken(inferredName)) {
          store.push({
            functionName: functionName,
            functionLocation: functionLocation,
            inferredName: inferredName,
            inferredChain: inferredChain,
            inferredLocation: inferredLocation
          });
        }
      },

      /**
       * Callback invoked for each arrow expression node.
       * @param Node node
       */
      onArrowFunctionExpression(node) {
        // Infer the function's name from an enclosing syntax tree node.
        let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(node);
        let inferredName = inferredInfo.name;
        let inferredChain = inferredInfo.chain;
        let inferredLocation = inferredInfo.loc;

        // Current node may be part of a larger assignment expression stack.
        if (node._parent.type == "AssignmentExpression") {
          this.onFunctionExpression(node._parent);
        }

        if (includesToken(inferredName)) {
          store.push({
            inferredName: inferredName,
            inferredChain: inferredChain,
            inferredLocation: inferredLocation
          });
        }
      }
    });

    return store;
  },

  AST: null,
  url: "",
  length: 0,
  offset: 0
};

/**
 * Parser utility methods.
 */
var ParserHelpers = {
  /**
   * Gets the location information for a node. Not all nodes have a
   * location property directly attached, or the location information
   * is incorrect, in which cases it's accessible via the parent.
   *
   * @param Node node
   *        The node who's location needs to be retrieved.
   * @return object
   *         An object containing { line, column } information.
   */
  getNodeLocation(node) {
    if (node.type != "Identifier") {
      return node.loc;
    }
    // Work around the fact that some identifier nodes don't have the
    // correct location attached.
    let { loc: parentLocation, type: parentType } = node._parent;
    let { loc: nodeLocation } = node;
    if (!nodeLocation) {
      if (parentType == "FunctionDeclaration" ||
          parentType == "FunctionExpression") {
        // e.g. "function foo() {}" or "{ bar: function foo() {} }"
        // The location is unavailable for the identifier node "foo".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.end.line = loc.start.line;
        loc.end.column = loc.start.column + node.name.length;
        return loc;
      }
      if (parentType == "MemberExpression") {
        // e.g. "foo.bar"
        // The location is unavailable for the identifier node "bar".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.start.line = loc.end.line;
        loc.start.column = loc.end.column - node.name.length;
        return loc;
      }
      if (parentType == "LabeledStatement") {
        // e.g. label: ...
        // The location is unavailable for the identifier node "label".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.end.line = loc.start.line;
        loc.end.column = loc.start.column + node.name.length;
        return loc;
      }
      if (parentType == "ContinueStatement" || parentType == "BreakStatement") {
        // e.g. continue label; or break label;
        // The location is unavailable for the identifier node "label".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.start.line = loc.end.line;
        loc.start.column = loc.end.column - node.name.length;
        return loc;
      }
    } else if (parentType == "VariableDeclarator") {
      // e.g. "let foo = 42"
      // The location incorrectly spans across the whole variable declaration,
      // not just the identifier node "foo".
      let loc = Cu.cloneInto(nodeLocation, {});
      loc.end.line = loc.start.line;
      loc.end.column = loc.start.column + node.name.length;
      return loc;
    }
    return node.loc;
  },

  /**
   * Checks if a node's bounds contains a specified line.
   *
   * @param Node node
   *        The node's bounds used as reference.
   * @param number line
   *        The line number to check.
   * @return boolean
   *         True if the line and column is contained in the node's bounds.
   */
  nodeContainsLine(node, line) {
    let { start: s, end: e } = this.getNodeLocation(node);
    return s.line <= line && e.line >= line;
  },

  /**
   * Checks if a node's bounds contains a specified line and column.
   *
   * @param Node node
   *        The node's bounds used as reference.
   * @param number line
   *        The line number to check.
   * @param number column
   *        The column number to check.
   * @return boolean
   *         True if the line and column is contained in the node's bounds.
   */
  nodeContainsPoint(node, line, column) {
    let { start: s, end: e } = this.getNodeLocation(node);
    return s.line == line && e.line == line &&
           s.column <= column && e.column >= column;
  },

  /**
   * Try to infer a function expression's name & other details based on the
   * enclosing VariableDeclarator, AssignmentExpression or ObjectExpression.
   *
   * @param Node node
   *        The function expression node to get the name for.
   * @return object
   *         The inferred function name, or empty string can't infer the name,
   *         along with the chain (a generic "context", like a prototype chain)
   *         and location if available.
   */
  inferFunctionExpressionInfo(node) {
    let parent = node._parent;

    // A function expression may be defined in a variable declarator,
    // e.g. var foo = function(){}, in which case it is possible to infer
    // the variable name.
    if (parent.type == "VariableDeclarator") {
      return {
        name: parent.id.name,
        chain: null,
        loc: this.getNodeLocation(parent.id)
      };
    }

    // Function expressions can also be defined in assignment expressions,
    // e.g. foo = function(){} or foo.bar = function(){}, in which case it is
    // possible to infer the assignee name ("foo" and "bar" respectively).
    if (parent.type == "AssignmentExpression") {
      let propertyChain = this._getMemberExpressionPropertyChain(parent.left);
      let propertyLeaf = propertyChain.pop();
      return {
        name: propertyLeaf,
        chain: propertyChain,
        loc: this.getNodeLocation(parent.left)
      };
    }

    // If a function expression is defined in an object expression,
    // e.g. { foo: function(){} }, then it is possible to infer the name
    // from the corresponding property.
    if (parent.type == "ObjectExpression") {
      let propertyKey = this._getObjectExpressionPropertyKeyForValue(node);
      let propertyChain = this._getObjectExpressionPropertyChain(parent);
      let propertyLeaf = propertyKey.name;
      return {
        name: propertyLeaf,
        chain: propertyChain,
        loc: this.getNodeLocation(propertyKey)
      };
    }

    // Can't infer the function expression's name.
    return {
      name: "",
      chain: null,
      loc: null
    };
  },

  /**
   * Gets the name of an object expression's property to which a specified
   * value is assigned.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if "node" represents the "bar" identifier in a hypothetical
   * "{ foo: bar }" object expression, the returned node is the "foo"
   * identifier.
   *
   * @param Node node
   *        The value node in an object expression.
   * @return object
   *         The key identifier node in the object expression.
   */
  _getObjectExpressionPropertyKeyForValue(node) {
    let parent = node._parent;
    if (parent.type != "ObjectExpression") {
      return null;
    }
    for (let property of parent.properties) {
      if (property.value == node) {
        return property.key;
      }
    }
    return null;
  },

  /**
   * Gets an object expression's property chain to its parent
   * variable declarator or assignment expression, if available.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if node represents the "baz: {}" object expression in a
   * hypothetical "foo = { bar: { baz: {} } }" assignment expression, the
   * returned chain is ["foo", "bar", "baz"].
   *
   * @param Node node
   *        The object expression node to begin the scan from.
   * @param array aStore [optional]
   *        The chain to store the nodes into.
   * @return array
   *         The chain to the parent variable declarator, as strings.
   */
  _getObjectExpressionPropertyChain(node, aStore = []) {
    switch (node.type) {
      case "ObjectExpression":
        this._getObjectExpressionPropertyChain(node._parent, aStore);
        let propertyKey = this._getObjectExpressionPropertyKeyForValue(node);
        if (propertyKey) {
          aStore.push(propertyKey.name);
        }
        break;
      // Handle "var foo = { ... }" variable declarators.
      case "VariableDeclarator":
        aStore.push(node.id.name);
        break;
      // Handle "foo.bar = { ... }" assignment expressions, since they're
      // commonly used when defining an object's prototype methods; e.g:
      // "Foo.prototype = { ... }".
      case "AssignmentExpression":
        this._getMemberExpressionPropertyChain(node.left, aStore);
        break;
      // Additionally handle stuff like "foo = bar.baz({ ... })", because it's
      // commonly used in prototype-based inheritance in many libraries; e.g:
      // "Foo = Bar.extend({ ... })".
      case "NewExpression":
      case "CallExpression":
        this._getObjectExpressionPropertyChain(node._parent, aStore);
        break;
    }
    return aStore;
  },

  /**
   * Gets a member expression's property chain.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if node represents a hypothetical "foo.bar.baz"
   * member expression, the returned chain ["foo", "bar", "baz"].
   *
   * More complex expressions like foo.bar().baz are intentionally not handled.
   *
   * @param Node node
   *        The member expression node to begin the scan from.
   * @param array store [optional]
   *        The chain to store the nodes into.
   * @return array
   *         The full member chain, as strings.
   */
  _getMemberExpressionPropertyChain(node, store = []) {
    switch (node.type) {
      case "MemberExpression":
        this._getMemberExpressionPropertyChain(node.object, store);
        this._getMemberExpressionPropertyChain(node.property, store);
        break;
      case "ThisExpression":
        store.push("this");
        break;
      case "Identifier":
        store.push(node.name);
        break;
    }
    return store;
  },

  /**
   * Returns an evaluation string which can be used to obtain the
   * current value for the respective identifier.
   *
   * @param Node node
   *        The leaf node (e.g. Identifier, Literal) to begin the scan from.
   * @return string
   *         The corresponding evaluation string, or empty string if
   *         the specified leaf node can't be used.
   */
  getIdentifierEvalString(node) {
    switch (node._parent.type) {
      case "ObjectExpression":
        // If the identifier is the actual property value, it can be used
        // directly as an evaluation string. Otherwise, construct the property
        // access chain, since the value might have changed.
        if (!this._getObjectExpressionPropertyKeyForValue(node)) {
          let propertyChain =
            this._getObjectExpressionPropertyChain(node._parent);
          let propertyLeaf = node.name;
          return [...propertyChain, propertyLeaf].join(".");
        }
        break;
      case "MemberExpression":
        // Make sure this is a property identifier, not the parent object.
        if (node._parent.property == node) {
          return this._getMemberExpressionPropertyChain(node._parent).join(".");
        }
        break;
    }
    switch (node.type) {
      case "ThisExpression":
        return "this";
      case "Identifier":
        return node.name;
      case "Literal":
        return uneval(node.value);
      default:
        return "";
    }
  }
};

/**
 * A visitor for a syntax tree generated by the reflection API.
 * See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API.
 *
 * All node types implement the following interface:
 * interface Node {
 *   type: string;
 *   loc: SourceLocation | null;
 * }
 */
var SyntaxTreeVisitor = {
  /**
   * Walks a syntax tree.
   *
   * @param object tree
   *        The AST nodes generated by the reflection API
   * @param object callbacks
   *        A map of all the callbacks to invoke when passing through certain
   *        types of noes (e.g: onFunctionDeclaration, onBlockStatement etc.).
   */
  walk(tree, callbacks) {
    this.break = false;
    this[tree.type](tree, callbacks);
  },

  /**
   * Filters all the nodes in this syntax tree based on a predicate.
   *
   * @param object tree
   *        The AST nodes generated by the reflection API
   * @param function predicate
   *        The predicate ran on each node.
   * @return array
   *         An array of nodes validating the predicate.
   */
  filter(tree, predicate) {
    let store = [];
    this.walk(tree, {
      onNode: e => {
        if (predicate(e)) {
          store.push(e);
        }
      }
    });
    return store;
  },

  /**
   * A flag checked on each node in the syntax tree. If true, walking is
   * abruptly halted.
   */
  break: false,

  /**
   * A complete program source tree.
   *
   * interface Program <: Node {
   *   type: "Program";
   *   body: [ Statement ];
   * }
   */
  Program(node, callbacks) {
    if (callbacks.onProgram) {
      callbacks.onProgram(node);
    }
    for (let statement of node.body) {
      this[statement.type](statement, node, callbacks);
    }
  },

  /**
   * Any statement.
   *
   * interface Statement <: Node { }
   */
  Statement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onStatement) {
      callbacks.onStatement(node);
    }
  },

  /**
   * An empty statement, i.e., a solitary semicolon.
   *
   * interface EmptyStatement <: Statement {
   *   type: "EmptyStatement";
   * }
   */
  EmptyStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onEmptyStatement) {
      callbacks.onEmptyStatement(node);
    }
  },

  /**
   * A block statement, i.e., a sequence of statements surrounded by braces.
   *
   * interface BlockStatement <: Statement {
   *   type: "BlockStatement";
   *   body: [ Statement ];
   * }
   */
  BlockStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onBlockStatement) {
      callbacks.onBlockStatement(node);
    }
    for (let statement of node.body) {
      this[statement.type](statement, node, callbacks);
    }
  },

  /**
   * An expression statement, i.e., a statement consisting of a single
   * expression.
   *
   * interface ExpressionStatement <: Statement {
   *   type: "ExpressionStatement";
   *   expression: Expression;
   * }
   */
  ExpressionStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onExpressionStatement) {
      callbacks.onExpressionStatement(node);
    }
    this[node.expression.type](node.expression, node, callbacks);
  },

  /**
   * An if statement.
   *
   * interface IfStatement <: Statement {
   *   type: "IfStatement";
   *   test: Expression;
   *   consequent: Statement;
   *   alternate: Statement | null;
   * }
   */
  IfStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onIfStatement) {
      callbacks.onIfStatement(node);
    }
    this[node.test.type](node.test, node, callbacks);
    this[node.consequent.type](node.consequent, node, callbacks);
    if (node.alternate) {
      this[node.alternate.type](node.alternate, node, callbacks);
    }
  },

  /**
   * A labeled statement, i.e., a statement prefixed by a break/continue label.
   *
   * interface LabeledStatement <: Statement {
   *   type: "LabeledStatement";
   *   label: Identifier;
   *   body: Statement;
   * }
   */
  LabeledStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onLabeledStatement) {
      callbacks.onLabeledStatement(node);
    }
    this[node.label.type](node.label, node, callbacks);
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A break statement.
   *
   * interface BreakStatement <: Statement {
   *   type: "BreakStatement";
   *   label: Identifier | null;
   * }
   */
  BreakStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onBreakStatement) {
      callbacks.onBreakStatement(node);
    }
    if (node.label) {
      this[node.label.type](node.label, node, callbacks);
    }
  },

  /**
   * A continue statement.
   *
   * interface ContinueStatement <: Statement {
   *   type: "ContinueStatement";
   *   label: Identifier | null;
   * }
   */
  ContinueStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onContinueStatement) {
      callbacks.onContinueStatement(node);
    }
    if (node.label) {
      this[node.label.type](node.label, node, callbacks);
    }
  },

  /**
   * A with statement.
   *
   * interface WithStatement <: Statement {
   *   type: "WithStatement";
   *   object: Expression;
   *   body: Statement;
   * }
   */
  WithStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onWithStatement) {
      callbacks.onWithStatement(node);
    }
    this[node.object.type](node.object, node, callbacks);
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A switch statement. The lexical flag is metadata indicating whether the
   * switch statement contains any unnested let declarations (and therefore
   * introduces a new lexical scope).
   *
   * interface SwitchStatement <: Statement {
   *   type: "SwitchStatement";
   *   discriminant: Expression;
   *   cases: [ SwitchCase ];
   *   lexical: boolean;
   * }
   */
  SwitchStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onSwitchStatement) {
      callbacks.onSwitchStatement(node);
    }
    this[node.discriminant.type](node.discriminant, node, callbacks);
    for (let _case of node.cases) {
      this[_case.type](_case, node, callbacks);
    }
  },

  /**
   * A return statement.
   *
   * interface ReturnStatement <: Statement {
   *   type: "ReturnStatement";
   *   argument: Expression | null;
   * }
   */
  ReturnStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onReturnStatement) {
      callbacks.onReturnStatement(node);
    }
    if (node.argument) {
      this[node.argument.type](node.argument, node, callbacks);
    }
  },

  /**
   * A throw statement.
   *
   * interface ThrowStatement <: Statement {
   *   type: "ThrowStatement";
   *   argument: Expression;
   * }
   */
  ThrowStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onThrowStatement) {
      callbacks.onThrowStatement(node);
    }
    this[node.argument.type](node.argument, node, callbacks);
  },

  /**
   * A try statement.
   *
   * interface TryStatement <: Statement {
   *   type: "TryStatement";
   *   block: BlockStatement;
   *   handler: CatchClause | null;
   *   guardedHandlers: [ CatchClause ];
   *   finalizer: BlockStatement | null;
   * }
   */
  TryStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onTryStatement) {
      callbacks.onTryStatement(node);
    }
    this[node.block.type](node.block, node, callbacks);
    if (node.handler) {
      this[node.handler.type](node.handler, node, callbacks);
    }
    for (let guardedHandler of node.guardedHandlers) {
      this[guardedHandler.type](guardedHandler, node, callbacks);
    }
    if (node.finalizer) {
      this[node.finalizer.type](node.finalizer, node, callbacks);
    }
  },

  /**
   * A while statement.
   *
   * interface WhileStatement <: Statement {
   *   type: "WhileStatement";
   *   test: Expression;
   *   body: Statement;
   * }
   */
  WhileStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onWhileStatement) {
      callbacks.onWhileStatement(node);
    }
    this[node.test.type](node.test, node, callbacks);
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A do/while statement.
   *
   * interface DoWhileStatement <: Statement {
   *   type: "DoWhileStatement";
   *   body: Statement;
   *   test: Expression;
   * }
   */
  DoWhileStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onDoWhileStatement) {
      callbacks.onDoWhileStatement(node);
    }
    this[node.body.type](node.body, node, callbacks);
    this[node.test.type](node.test, node, callbacks);
  },

  /**
   * A for statement.
   *
   * interface ForStatement <: Statement {
   *   type: "ForStatement";
   *   init: VariableDeclaration | Expression | null;
   *   test: Expression | null;
   *   update: Expression | null;
   *   body: Statement;
   * }
   */
  ForStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onForStatement) {
      callbacks.onForStatement(node);
    }
    if (node.init) {
      this[node.init.type](node.init, node, callbacks);
    }
    if (node.test) {
      this[node.test.type](node.test, node, callbacks);
    }
    if (node.update) {
      this[node.update.type](node.update, node, callbacks);
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A for/in statement, or, if each is true, a for each/in statement.
   *
   * interface ForInStatement <: Statement {
   *   type: "ForInStatement";
   *   left: VariableDeclaration | Expression;
   *   right: Expression;
   *   body: Statement;
   *   each: boolean;
   * }
   */
  ForInStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onForInStatement) {
      callbacks.onForInStatement(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A for/of statement.
   *
   * interface ForOfStatement <: Statement {
   *   type: "ForOfStatement";
   *   left: VariableDeclaration | Expression;
   *   right: Expression;
   *   body: Statement;
   * }
   */
  ForOfStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onForOfStatement) {
      callbacks.onForOfStatement(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A let statement.
   *
   * interface LetStatement <: Statement {
   *   type: "LetStatement";
   *   head: [ { id: Pattern, init: Expression | null } ];
   *   body: Statement;
   * }
   */
  LetStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onLetStatement) {
      callbacks.onLetStatement(node);
    }
    for (let { id, init } of node.head) {
      this[id.type](id, node, callbacks);
      if (init) {
        this[init.type](init, node, callbacks);
      }
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A debugger statement.
   *
   * interface DebuggerStatement <: Statement {
   *   type: "DebuggerStatement";
   * }
   */
  DebuggerStatement(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onDebuggerStatement) {
      callbacks.onDebuggerStatement(node);
    }
  },

  /**
   * Any declaration node. Note that declarations are considered statements;
   * this is because declarations can appear in any statement context in the
   * language recognized by the SpiderMonkey parser.
   *
   * interface Declaration <: Statement { }
   */
  Declaration(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onDeclaration) {
      callbacks.onDeclaration(node);
    }
  },

  /**
   * A function declaration.
   *
   * interface FunctionDeclaration <: Function, Declaration {
   *   type: "FunctionDeclaration";
   *   id: Identifier;
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  FunctionDeclaration(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onFunctionDeclaration) {
      callbacks.onFunctionDeclaration(node);
    }
    this[node.id.type](node.id, node, callbacks);
    for (let param of node.params) {
      this[param.type](param, node, callbacks);
    }
    for (let _default of node.defaults) {
      if (_default) {
        this[_default.type](_default, node, callbacks);
      }
    }
    if (node.rest) {
      this[node.rest.type](node.rest, node, callbacks);
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A variable declaration, via one of var, let, or const.
   *
   * interface VariableDeclaration <: Declaration {
   *   type: "VariableDeclaration";
   *   declarations: [ VariableDeclarator ];
   *   kind: "var" | "let" | "const";
   * }
   */
  VariableDeclaration(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onVariableDeclaration) {
      callbacks.onVariableDeclaration(node);
    }
    for (let declaration of node.declarations) {
      this[declaration.type](declaration, node, callbacks);
    }
  },

  /**
   * A variable declarator.
   *
   * interface VariableDeclarator <: Node {
   *   type: "VariableDeclarator";
   *   id: Pattern;
   *   init: Expression | null;
   * }
   */
  VariableDeclarator(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onVariableDeclarator) {
      callbacks.onVariableDeclarator(node);
    }
    this[node.id.type](node.id, node, callbacks);
    if (node.init) {
      this[node.init.type](node.init, node, callbacks);
    }
  },

  /**
   * Any expression node. Since the left-hand side of an assignment may be any
   * expression in general, an expression can also be a pattern.
   *
   * interface Expression <: Node, Pattern { }
   */
  Expression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onExpression) {
      callbacks.onExpression(node);
    }
  },

  /**
   * A this expression.
   *
   * interface ThisExpression <: Expression {
   *   type: "ThisExpression";
   * }
   */
  ThisExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onThisExpression) {
      callbacks.onThisExpression(node);
    }
  },

  /**
   * An array expression.
   *
   * interface ArrayExpression <: Expression {
   *   type: "ArrayExpression";
   *   elements: [ Expression | null ];
   * }
   */
  ArrayExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onArrayExpression) {
      callbacks.onArrayExpression(node);
    }
    for (let element of node.elements) {
      if (element) {
        this[element.type](element, node, callbacks);
      }
    }
  },

  /**
   * A spread expression.
   *
   * interface SpreadExpression <: Expression {
   *   type: "SpreadExpression";
   *   expression: Expression;
   * }
   */
  SpreadExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onSpreadExpression) {
      callbacks.onSpreadExpression(node);
    }
    this[node.expression.type](node.expression, node, callbacks);
  },

  /**
   * An object expression. A literal property in an object expression can have
   * either a string or number as its value. Ordinary property initializers
   * have a kind value "init"; getters and setters have the kind values "get"
   * and "set", respectively.
   *
   * interface ObjectExpression <: Expression {
   *   type: "ObjectExpression";
   *   properties: [ { key: Literal | Identifier | ComputedName,
   *                   value: Expression,
   *                   kind: "init" | "get" | "set" } ];
   * }
   */
  ObjectExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onObjectExpression) {
      callbacks.onObjectExpression(node);
    }
    for (let { key, value } of node.properties) {
      this[key.type](key, node, callbacks);
      this[value.type](value, node, callbacks);
    }
  },

  /**
   * A computed property name in object expression, like in { [a]: b }
   *
   * interface ComputedName <: Node {
   *   type: "ComputedName";
   *   name: Expression;
   * }
   */
  ComputedName(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onComputedName) {
      callbacks.onComputedName(node);
    }
    this[node.name.type](node.name, node, callbacks);
  },

  /**
   * A function expression.
   *
   * interface FunctionExpression <: Function, Expression {
   *   type: "FunctionExpression";
   *   id: Identifier | null;
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  FunctionExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onFunctionExpression) {
      callbacks.onFunctionExpression(node);
    }
    if (node.id) {
      this[node.id.type](node.id, node, callbacks);
    }
    for (let param of node.params) {
      this[param.type](param, node, callbacks);
    }
    for (let _default of node.defaults) {
      if (_default) {
        this[_default.type](_default, node, callbacks);
      }
    }
    if (node.rest) {
      this[node.rest.type](node.rest, node, callbacks);
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * An arrow expression.
   *
   * interface ArrowFunctionExpression <: Function, Expression {
   *   type: "ArrowFunctionExpression";
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  ArrowFunctionExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onArrowFunctionExpression) {
      callbacks.onArrowFunctionExpression(node);
    }
    for (let param of node.params) {
      this[param.type](param, node, callbacks);
    }
    for (let _default of node.defaults) {
      if (_default) {
        this[_default.type](_default, node, callbacks);
      }
    }
    if (node.rest) {
      this[node.rest.type](node.rest, node, callbacks);
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A sequence expression, i.e., a comma-separated sequence of expressions.
   *
   * interface SequenceExpression <: Expression {
   *   type: "SequenceExpression";
   *   expressions: [ Expression ];
   * }
   */
  SequenceExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onSequenceExpression) {
      callbacks.onSequenceExpression(node);
    }
    for (let expression of node.expressions) {
      this[expression.type](expression, node, callbacks);
    }
  },

  /**
   * A unary operator expression.
   *
   * interface UnaryExpression <: Expression {
   *   type: "UnaryExpression";
   *   operator: UnaryOperator;
   *   prefix: boolean;
   *   argument: Expression;
   * }
   */
  UnaryExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onUnaryExpression) {
      callbacks.onUnaryExpression(node);
    }
    this[node.argument.type](node.argument, node, callbacks);
  },

  /**
   * A binary operator expression.
   *
   * interface BinaryExpression <: Expression {
   *   type: "BinaryExpression";
   *   operator: BinaryOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  BinaryExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onBinaryExpression) {
      callbacks.onBinaryExpression(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
  },

  /**
   * An assignment operator expression.
   *
   * interface AssignmentExpression <: Expression {
   *   type: "AssignmentExpression";
   *   operator: AssignmentOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  AssignmentExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onAssignmentExpression) {
      callbacks.onAssignmentExpression(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
  },

  /**
   * An update (increment or decrement) operator expression.
   *
   * interface UpdateExpression <: Expression {
   *   type: "UpdateExpression";
   *   operator: UpdateOperator;
   *   argument: Expression;
   *   prefix: boolean;
   * }
   */
  UpdateExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onUpdateExpression) {
      callbacks.onUpdateExpression(node);
    }
    this[node.argument.type](node.argument, node, callbacks);
  },

  /**
   * A logical operator expression.
   *
   * interface LogicalExpression <: Expression {
   *   type: "LogicalExpression";
   *   operator: LogicalOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  LogicalExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onLogicalExpression) {
      callbacks.onLogicalExpression(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
  },

  /**
   * A conditional expression, i.e., a ternary ?/: expression.
   *
   * interface ConditionalExpression <: Expression {
   *   type: "ConditionalExpression";
   *   test: Expression;
   *   alternate: Expression;
   *   consequent: Expression;
   * }
   */
  ConditionalExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onConditionalExpression) {
      callbacks.onConditionalExpression(node);
    }
    this[node.test.type](node.test, node, callbacks);
    this[node.alternate.type](node.alternate, node, callbacks);
    this[node.consequent.type](node.consequent, node, callbacks);
  },

  /**
   * A new expression.
   *
   * interface NewExpression <: Expression {
   *   type: "NewExpression";
   *   callee: Expression;
   *   arguments: [ Expression | null ];
   * }
   */
  NewExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onNewExpression) {
      callbacks.onNewExpression(node);
    }
    this[node.callee.type](node.callee, node, callbacks);
    for (let argument of node.arguments) {
      if (argument) {
        this[argument.type](argument, node, callbacks);
      }
    }
  },

  /**
   * A function or method call expression.
   *
   * interface CallExpression <: Expression {
   *   type: "CallExpression";
   *   callee: Expression;
   *   arguments: [ Expression | null ];
   * }
   */
  CallExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onCallExpression) {
      callbacks.onCallExpression(node);
    }
    this[node.callee.type](node.callee, node, callbacks);
    for (let argument of node.arguments) {
      if (argument) {
        if (!this[argument.type]) {
          console.error("Unknown parser object:", argument.type);
        }
        this[argument.type](argument, node, callbacks);
      }
    }
  },

  /**
   * A member expression. If computed is true, the node corresponds to a
   * computed e1[e2] expression and property is an Expression. If computed is
   * false, the node corresponds to a static e1.x expression and property is an
   * Identifier.
   *
   * interface MemberExpression <: Expression {
   *   type: "MemberExpression";
   *   object: Expression;
   *   property: Identifier | Expression;
   *   computed: boolean;
   * }
   */
  MemberExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onMemberExpression) {
      callbacks.onMemberExpression(node);
    }
    this[node.object.type](node.object, node, callbacks);
    this[node.property.type](node.property, node, callbacks);
  },

  /**
   * A yield expression.
   *
   * interface YieldExpression <: Expression {
   *   argument: Expression | null;
   * }
   */
  YieldExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onYieldExpression) {
      callbacks.onYieldExpression(node);
    }
    if (node.argument) {
      this[node.argument.type](node.argument, node, callbacks);
    }
  },

  /**
   * An array comprehension. The blocks array corresponds to the sequence of
   * for and for each blocks. The optional filter expression corresponds to the
   * final if clause, if present.
   *
   * interface ComprehensionExpression <: Expression {
   *   body: Expression;
   *   blocks: [ ComprehensionBlock ];
   *   filter: Expression | null;
   * }
   */
  ComprehensionExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onComprehensionExpression) {
      callbacks.onComprehensionExpression(node);
    }
    this[node.body.type](node.body, node, callbacks);
    for (let block of node.blocks) {
      this[block.type](block, node, callbacks);
    }
    if (node.filter) {
      this[node.filter.type](node.filter, node, callbacks);
    }
  },

  /**
   * A generator expression. As with array comprehensions, the blocks array
   * corresponds to the sequence of for and for each blocks, and the optional
   * filter expression corresponds to the final if clause, if present.
   *
   * interface GeneratorExpression <: Expression {
   *   body: Expression;
   *   blocks: [ ComprehensionBlock ];
   *   filter: Expression | null;
   * }
   */
  GeneratorExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onGeneratorExpression) {
      callbacks.onGeneratorExpression(node);
    }
    this[node.body.type](node.body, node, callbacks);
    for (let block of node.blocks) {
      this[block.type](block, node, callbacks);
    }
    if (node.filter) {
      this[node.filter.type](node.filter, node, callbacks);
    }
  },

  /**
   * A graph expression, aka "sharp literal," such as #1={ self: #1# }.
   *
   * interface GraphExpression <: Expression {
   *   index: uint32;
   *   expression: Literal;
   * }
   */
  GraphExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onGraphExpression) {
      callbacks.onGraphExpression(node);
    }
    this[node.expression.type](node.expression, node, callbacks);
  },

  /**
   * A graph index expression, aka "sharp variable," such as #1#.
   *
   * interface GraphIndexExpression <: Expression {
   *   index: uint32;
   * }
   */
  GraphIndexExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onGraphIndexExpression) {
      callbacks.onGraphIndexExpression(node);
    }
  },

  /**
   * A let expression.
   *
   * interface LetExpression <: Expression {
   *   type: "LetExpression";
   *   head: [ { id: Pattern, init: Expression | null } ];
   *   body: Expression;
   * }
   */
  LetExpression(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onLetExpression) {
      callbacks.onLetExpression(node);
    }
    for (let { id, init } of node.head) {
      this[id.type](id, node, callbacks);
      if (init) {
        this[init.type](init, node, callbacks);
      }
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * Any pattern.
   *
   * interface Pattern <: Node { }
   */
  Pattern(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onPattern) {
      callbacks.onPattern(node);
    }
  },

  /**
   * An object-destructuring pattern. A literal property in an object pattern
   * can have either a string or number as its value.
   *
   * interface ObjectPattern <: Pattern {
   *   type: "ObjectPattern";
   *   properties: [ { key: Literal | Identifier, value: Pattern } ];
   * }
   */
  ObjectPattern(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onObjectPattern) {
      callbacks.onObjectPattern(node);
    }
    for (let { key, value } of node.properties) {
      this[key.type](key, node, callbacks);
      this[value.type](value, node, callbacks);
    }
  },

  /**
   * An array-destructuring pattern.
   *
   * interface ArrayPattern <: Pattern {
   *   type: "ArrayPattern";
   *   elements: [ Pattern | null ];
   * }
   */
  ArrayPattern(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onArrayPattern) {
      callbacks.onArrayPattern(node);
    }
    for (let element of node.elements) {
      if (element) {
        this[element.type](element, node, callbacks);
      }
    }
  },

  /**
   * A case (if test is an Expression) or default (if test is null) clause in
   * the body of a switch statement.
   *
   * interface SwitchCase <: Node {
   *   type: "SwitchCase";
   *   test: Expression | null;
   *   consequent: [ Statement ];
   * }
   */
  SwitchCase(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onSwitchCase) {
      callbacks.onSwitchCase(node);
    }
    if (node.test) {
      this[node.test.type](node.test, node, callbacks);
    }
    for (let consequent of node.consequent) {
      this[consequent.type](consequent, node, callbacks);
    }
  },

  /**
   * A catch clause following a try block. The optional guard property
   * corresponds to the optional expression guard on the bound variable.
   *
   * interface CatchClause <: Node {
   *   type: "CatchClause";
   *   param: Pattern;
   *   guard: Expression | null;
   *   body: BlockStatement;
   * }
   */
  CatchClause(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onCatchClause) {
      callbacks.onCatchClause(node);
    }
    this[node.param.type](node.param, node, callbacks);
    if (node.guard) {
      this[node.guard.type](node.guard, node, callbacks);
    }
    this[node.body.type](node.body, node, callbacks);
  },

  /**
   * A for or for each block in an array comprehension or generator expression.
   *
   * interface ComprehensionBlock <: Node {
   *   left: Pattern;
   *   right: Expression;
   *   each: boolean;
   * }
   */
  ComprehensionBlock(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onComprehensionBlock) {
      callbacks.onComprehensionBlock(node);
    }
    this[node.left.type](node.left, node, callbacks);
    this[node.right.type](node.right, node, callbacks);
  },

  /**
   * An identifier. Note that an identifier may be an expression or a
   * destructuring pattern.
   *
   * interface Identifier <: Node, Expression, Pattern {
   *   type: "Identifier";
   *   name: string;
   * }
   */
  Identifier(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onIdentifier) {
      callbacks.onIdentifier(node);
    }
  },

  /**
   * A literal token. Note that a literal can be an expression.
   *
   * interface Literal <: Node, Expression {
   *   type: "Literal";
   *   value: string | boolean | null | number | RegExp;
   * }
   */
  Literal(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onLiteral) {
      callbacks.onLiteral(node);
    }
  },

  /**
   * A template string literal.
   *
   * interface TemplateLiteral <: Node {
   *   type: "TemplateLiteral";
   *   elements: [ Expression ];
   * }
   */
  TemplateLiteral(node, parent, callbacks) {
    node._parent = parent;

    if (this.break) {
      return;
    }
    if (callbacks.onNode) {
      if (callbacks.onNode(node, parent) === false) {
        return;
      }
    }
    if (callbacks.onTemplateLiteral) {
      callbacks.onTemplateLiteral(node);
    }
    for (let element of node.elements) {
      if (element) {
        this[element.type](element, node, callbacks);
      }
    }
  }
};

XPCOMUtils.defineLazyGetter(Parser, "reflectionAPI", () => Reflect);
